Skip to content

feat(settings): migrate settings from modal to route-based pages#3413

Merged
waleedlatif1 merged 1 commit intofeat/mothership-copilotfrom
feat/settings-sidebar
Mar 4, 2026
Merged

feat(settings): migrate settings from modal to route-based pages#3413
waleedlatif1 merged 1 commit intofeat/mothership-copilotfrom
feat/settings-sidebar

Conversation

@waleedlatif1
Copy link
Collaborator

Summary

  • Migrated settings from modal to dedicated route-based pages at /workspace/[workspaceId]/settings/[section]
  • Added SettingsSidebar component with navigation matching workflow sidebar styling
  • Created useSettingsNavigation hook replacing all useSettingsModalStore + window event dispatchers
  • Deleted old settings modal store, settings modal component, and all open-settings/close-settings window events
  • Updated Files page layout to match Tables/Knowledge Base pattern (consistent header, search bar, spacing)
  • Fixed modal centering to account for sidebar width using CSS calc()
  • Bumped settings page fonts and spacing ~10% for readability
  • Moved settings components from settings-modal/ to settings/components/

Type of Change

  • New feature
  • Refactor

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link

vercel bot commented Mar 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Mar 4, 2026 11:17pm

Request Review

@cursor
Copy link

cursor bot commented Mar 4, 2026

PR Summary

Medium Risk
Migrates navigation and state handling for settings from a modal to routed pages and changes several redirect targets to /home, which could break deep links or user flows if any paths/permissions checks are missed. Adds a superuser-only Debug settings action that triggers server-side workflow imports, so access control and UX regressions should be validated.

Overview
Settings is migrated from a modal to dedicated pages under /workspace/[workspaceId]/settings/[section], with a new SettingsPage section switcher and useSettingsNavigation to route users to specific sections (replacing modal-store/window-event patterns).

Multiple settings components are refit for the route-based layout (imports/paths updated, modal-specific onOpenChange/unsaved-close handling removed, and broad font/spacing adjustments for readability). A new superuser-only Debug section is added to import workflows by ID via /api/superuser/import-workflow.

Navigation/UX adjustments: workspace root and invitation accept redirects now go to /home (tests updated), the Files page is restructured with a new header/layout and FileList component, and the Knowledge action bar is re-centered accounting for sidebar width. Minor refactors update internal import paths (e.g., copilot stream buffer, plan configs).

Written by Cursor Bugbot for commit 15b28e1. Configure here.

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Greptile Summary

This PR migrates the settings experience from a modal dialog to dedicated route-based pages at /workspace/[workspaceId]/settings/[section], replacing the old useSettingsModalStore + window.dispatchEvent pattern with a clean useSettingsNavigation hook and a SettingsSidebar component that renders inside the existing workspace Sidebar shell when on settings routes.

Key changes and observations:

  • Settings routing: A new [section]/page.tsx server component guards access (session + workspace membership), then delegates to the SettingsPage client component. The settings/page.tsx root immediately redirects to general.
  • Sidebar toggling: sidebar.tsx now checks isOnSettingsPage and renders SettingsSidebar (section nav) instead of the normal workflow/task sidebar — a clean approach that reuses the existing sidebar shell.
  • Missing fallback for invalid sections: SettingsPage has no guard against arbitrary section URL parameters — navigating to /settings/nonexistent renders only the heading with a blank content area instead of redirecting to general or showing a 404.
  • Bundle size concern: All 16+ settings section components (including very large files like mcp.tsx and credentials-manager.tsx) are statically imported in settings-page.tsx, bundling the entire settings surface into one chunk even though only one section is shown at a time.
  • mcpServerId URL encoding: useSettingsNavigation interpolates the server ID directly into the query string without encodeURIComponent.
  • bg-white hardcoded in files.tsx: The new Files view hardcodes bg-white for light mode rather than using a CSS variable, which could break white-label theming.
  • All window.dispatchEvent('open-settings') / close-settings event dispatchers are fully removed with no dangling references remaining — the migration is complete.

Confidence Score: 3/5

  • Mostly safe to merge but has a UX hole (blank page on invalid section) and a bundle size regression worth addressing before landing.
  • The migration is architecturally sound and the old modal pattern is fully cleaned up. The main concerns are: (1) an invalid section URL parameter produces a blank settings page with no redirect fallback, (2) all settings components are statically imported in a single chunk which will noticeably increase the initial settings page bundle, and (3) the mcpServerId query param is not URL-encoded. None of these are security issues or data-loss bugs, but the blank page case is a user-visible regression from the modal (which would simply not open for unknown sections).
  • apps/sim/app/workspace/[workspaceId]/settings/[section]/settings-page.tsx needs the invalid-section fallback and dynamic imports; apps/sim/hooks/use-settings-navigation.ts needs encodeURIComponent.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/settings/[section]/settings-page.tsx New client component rendering section content — no fallback for invalid section params (blank page), and all section components are statically imported increasing bundle size significantly.
apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx New server page with session and workspace membership validation before rendering settings; correctly guards access but does not validate section parameter against allowed values.
apps/sim/app/workspace/[workspaceId]/settings/navigation.ts New config file defining SettingsSection types, NavigationItem interface, and all navigation items with their permission requirements; well-structured migration of the old modal's navigation config.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx New sidebar component replacing the settings modal's navigation panel; correctly applies permission-based filtering and active state detection from pathname, but back button hard-codes /home navigation (already flagged in prior review).
apps/sim/hooks/use-settings-navigation.ts New hook replacing all window event dispatches for opening settings; clean abstraction, but mcpServerId value is interpolated into URL without encodeURIComponent.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx Refactored sidebar to conditionally render SettingsSidebar when on settings route, replacing the old modal approach; the isOnSettingsPage check is clean and handles null pathname correctly.
apps/sim/app/workspace/[workspaceId]/files/files.tsx New files page matching the Tables/KB layout pattern; hardcodes bg-white for light mode instead of using a CSS variable which may break white-label theming.
apps/sim/app/workspace/[workspaceId]/settings/layout.tsx Simple layout wrapper providing max-width centering and vertical scroll for the settings content area; straightforward and correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User clicks Settings] --> B[Router navigates to\n/workspace/workspaceId/settings/general]
    B --> C[workspaceId layout.tsx\nrenders Sidebar + children]
    C --> D{isOnSettingsPage?}
    D -- Yes --> E[Render SettingsSidebar\n in sidebar slot]
    D -- No --> F[Render normal\nWorkflow Sidebar]
    E --> G[settings/layout.tsx\nmax-w-900px scrollable]
    G --> H[settings/section/page.tsx\nServer: auth + workspace check]
    H --> I{Valid session\n+ membership?}
    I -- No --> J[redirect '/']
    I -- Yes --> K[SettingsPage client component\nreads section from props\nreads mcpServerId from useSearchParams]
    K --> L{effectiveSection?}
    L -- general --> M[General]
    L -- credentials --> N[Credentials]
    L -- mcp --> O[MCP]
    L -- subscription --> P[Subscription]
    L -- ... --> Q[Other sections]
    L -- unknown --> R[⚠️ Blank page\nno fallback]
    E --> S[Back button\nhard-codes /home]
Loading

Last reviewed commit: 15b28e1

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1 waleedlatif1 force-pushed the feat/settings-sidebar branch from acff8fc to 15b28e1 Compare March 4, 2026 23:14
@waleedlatif1
Copy link
Collaborator Author

@cursor review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@waleedlatif1 waleedlatif1 merged commit b07925f into feat/mothership-copilot Mar 4, 2026
4 checks passed
@waleedlatif1 waleedlatif1 deleted the feat/settings-sidebar branch March 4, 2026 23:20
Comment on lines +32 to +62
export function SettingsPage({ section }: SettingsPageProps) {
const searchParams = useSearchParams()
const mcpServerId = searchParams.get('mcpServerId')

const effectiveSection =
!isBillingEnabled && (section === 'subscription' || section === 'team') ? 'general' : section

const label =
allNavigationItems.find((item) => item.id === effectiveSection)?.label ?? effectiveSection

return (
<div>
<h2 className='mb-[28px] font-medium text-[22px] text-[var(--text-primary)]'>{label}</h2>
{effectiveSection === 'general' && <General />}
{effectiveSection === 'credentials' && <Credentials />}
{effectiveSection === 'template-profile' && <TemplateProfile />}
{effectiveSection === 'credential-sets' && <CredentialSets />}
{effectiveSection === 'access-control' && <AccessControl />}
{effectiveSection === 'apikeys' && <ApiKeys />}
{isBillingEnabled && effectiveSection === 'subscription' && <Subscription />}
{isBillingEnabled && effectiveSection === 'team' && <TeamManagement />}
{effectiveSection === 'sso' && <SSO />}
{effectiveSection === 'byok' && <BYOK />}
{effectiveSection === 'copilot' && <Copilot />}
{effectiveSection === 'mcp' && <MCP initialServerId={mcpServerId} />}
{effectiveSection === 'custom-tools' && <CustomTools />}
{effectiveSection === 'skills' && <Skills />}
{effectiveSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
{effectiveSection === 'debug' && <Debug />}
</div>
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid section parameter renders blank settings page

When a user navigates to an unrecognized section (e.g. /workspace/[id]/settings/nonexistent), effectiveSection will be a string that matches none of the conditional renders below the <h2>. The result is a page that shows only the section string as a heading with no content — no 404 or redirect to general.

The section parameter should be validated server-side in page.tsx before passing it to this component, or a fallback should be added here:

Suggested change
export function SettingsPage({ section }: SettingsPageProps) {
const searchParams = useSearchParams()
const mcpServerId = searchParams.get('mcpServerId')
const effectiveSection =
!isBillingEnabled && (section === 'subscription' || section === 'team') ? 'general' : section
const label =
allNavigationItems.find((item) => item.id === effectiveSection)?.label ?? effectiveSection
return (
<div>
<h2 className='mb-[28px] font-medium text-[22px] text-[var(--text-primary)]'>{label}</h2>
{effectiveSection === 'general' && <General />}
{effectiveSection === 'credentials' && <Credentials />}
{effectiveSection === 'template-profile' && <TemplateProfile />}
{effectiveSection === 'credential-sets' && <CredentialSets />}
{effectiveSection === 'access-control' && <AccessControl />}
{effectiveSection === 'apikeys' && <ApiKeys />}
{isBillingEnabled && effectiveSection === 'subscription' && <Subscription />}
{isBillingEnabled && effectiveSection === 'team' && <TeamManagement />}
{effectiveSection === 'sso' && <SSO />}
{effectiveSection === 'byok' && <BYOK />}
{effectiveSection === 'copilot' && <Copilot />}
{effectiveSection === 'mcp' && <MCP initialServerId={mcpServerId} />}
{effectiveSection === 'custom-tools' && <CustomTools />}
{effectiveSection === 'skills' && <Skills />}
{effectiveSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
{effectiveSection === 'debug' && <Debug />}
</div>
)
const validSections = allNavigationItems.map((item) => item.id)
const effectiveSection: SettingsSection = (() => {
if (!isBillingEnabled && (section === 'subscription' || section === 'team')) {
return 'general'
}
if (!validSections.includes(section as SettingsSection)) {
return 'general'
}
return section
})()

Comment on lines +1 to +28
'use client'

import { useSearchParams } from 'next/navigation'
import {
ApiKeys,
BYOK,
Copilot,
CredentialSets,
Credentials,
CustomTools,
Debug,
General,
MCP,
Skills,
Subscription,
TeamManagement,
TemplateProfile,
WorkflowMcpServers,
} from '@/app/workspace/[workspaceId]/settings/components'
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
import {
allNavigationItems,
isBillingEnabled,
} from '@/app/workspace/[workspaceId]/settings/navigation'
import { AccessControl } from '@/ee/access-control/components/access-control'
import { SSO } from '@/ee/sso/components/sso-settings'

interface SettingsPageProps {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All settings components statically imported — large bundle chunk

Every settings section component (General, MCP, Credentials, TeamManagement, etc.) is statically imported at the top of this file. Since the user only ever views one section at a time, this means the entire settings surface — including very large components like mcp.tsx (~1626 lines) and credentials-manager.tsx (~1949 lines) — is bundled into a single client chunk.

Consider using next/dynamic for the heavier section components to split them into separate lazy-loaded chunks:

import dynamic from 'next/dynamic'

const MCP = dynamic(() => import('../components/mcp/mcp').then((m) => ({ default: m.MCP })))
const Credentials = dynamic(() => import('../components/credentials/credentials').then((m) => ({ default: m.Credentials })))
// ...

Comment on lines +10 to +12
<div className='flex flex-1 flex-col overflow-auto bg-white px-[24px] pt-[28px] pb-[24px] dark:bg-[var(--bg)]'>
{/* Header */}
<div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded bg-white bypasses theme CSS variable

The inner container uses bg-white for light mode rather than a CSS variable (--bg or --surface-2). If the theme defines a non-white light background (e.g. in a white-label config), this element will appear inconsistent with the rest of the UI. The dark mode counterpart already uses dark:bg-[var(--bg)] correctly.

Suggested change
<div className='flex flex-1 flex-col overflow-auto bg-white px-[24px] pt-[28px] pb-[24px] dark:bg-[var(--bg)]'>
{/* Header */}
<div>
<div className='flex flex-1 flex-col overflow-auto bg-[var(--bg)] px-[24px] pt-[28px] pb-[24px]'>

Comment on lines +23 to +27
(options?: SettingsNavigationOptions): string => {
const section = options?.section || 'general'
const searchParams = options?.mcpServerId ? `?mcpServerId=${options.mcpServerId}` : ''
return `/workspace/${workspaceId}/settings/${section}${searchParams}`
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mcpServerId appended to URL without encoding

The mcpServerId value is interpolated directly into the query string without encodeURIComponent. If a server ID ever contains characters like +, &, #, or spaces, the resulting URL will be malformed and useSearchParams().get('mcpServerId') will return an incorrect value.

Suggested change
(options?: SettingsNavigationOptions): string => {
const section = options?.section || 'general'
const searchParams = options?.mcpServerId ? `?mcpServerId=${options.mcpServerId}` : ''
return `/workspace/${workspaceId}/settings/${section}${searchParams}`
},
(options?: SettingsNavigationOptions): string => {
const section = options?.section || 'general'
const searchParams = options?.mcpServerId
? `?mcpServerId=${encodeURIComponent(options.mcpServerId)}`
: ''
return `/workspace/${workspaceId}/settings/${section}${searchParams}`
},

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant